Android Binder分析

关于进程间通信(IPC)

为什么需要进程间通信

前面我们使用了AIDL,知道了AIDL可以使我们快速的进行进程间通信,为什么我们需要进程间通信呢?最早的操作系统内存分配方式很简单,直接给每一个程序分配物理地址,这样就会出现很多问题:

  1. 进程可以随意访问整个内存,安全性极低
  2. 内存使用效率低,因为内存直接被分配,如果内存不足时,会释放,多进程交替的情况,会不断的装入装出
  3. 随意分配导致程序运行的地址不确定

为了解决这些问题,操作系统引入了虚拟地址以及一些列方式去解决,新的方式使用虚拟地址的方式,隔离了进程和实际的物理内存,通过虚拟地址和物理地址的映射让进程能够访问实际的内存,对于不同的进程,分配的虚拟地址不同,比如A程序的分配到的虚拟地址空间分布在0x00000000到0x00A00000,程序B为0x00C00000到0x07000000,彼此不重叠,所以对于A或者B只能访问到自己的那一块内存区域。这里提到的虚拟地址解决了早期操作系统上面的1和3问题,至于2怎么解决的大家可以去了解下。有了虚拟地址,进程间就隔离了,但是实际的情况中,我们还是需要一些进程间的通信,于是操作系统给我们提供了一些机制,可以让我们进行一些在操作系统控制下的进程间通信,所以我们必须知道在某一种操作系统中,它所提供的方式是什么,然后去使用它进行进程间通信。

常见的进程通信方式

操作系统中进程通讯的方式有很多,操作系统根据实际的需求去选择,这里举几个常见方式:

共享内存

这里的共享内存和之前提到过最早的操作系统内存分配方式是不一样的,这种方式的每个进程还是访问自己的内存,但是某个进程可以共享一部分内存出来供其他进程访问。但是这种方式还是会有安全性问题,因为共享的那部分内存任何进程都可以访问,所以这种方式优点是效率高,缺点是安全性低、控制复杂。

管道

管道通过采用储存转发的方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。所以开销比较大

Socket

这种方式是Client-Server的通信方式,它主要用在跨网络的进程间通信和本机上进程间的低速通信,它的效率和开销都非常大

Android系统采用哪一种通信方式

除了上面提到的三种通信方式,还有一些其他的方式,那么Android系统是用的上面哪一种方式呢?以上都不是,因为他们有不同的缺点,这些缺点在Android这种资源有限的设备中是不能被接受的,所以Android系统自己实现了一套进程间通信方式:Binder机制,在正式开始介绍Binder前,还需要了解两个概念内核空间用户空间

内核空间/用户空间

Android最底层Linux Kernel,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限,它单独运行在内核空间,其他程序是不能调用和访问这一块空间的,普通的程序运行在用户空间。

虽然从逻辑上抽离出用户空间和内核空间;但是不可避免的的是,总有那么一些用户空间需要访问内核的资源;比如应用程序访问文件,网络是很常见的事情,怎么办呢?

Kernel space can be accessed by user processes only through the use of system calls.

用户空间访问内核空间的唯一方式就是系统调用;通过这个统一入口接口,所有的资源访问都是在内核的控制下执行,以免导致对用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。用户软件良莠不齐,要是它们乱搞把系统玩坏了怎么办?因此对于某些特权操作必须交给安全可靠的内核来执行,当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)此时处理器处于特权级最高的(0级)内核代码中执行。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。处理器在特权等级高的时候才能执行那些特权CPU指令。

内核模块

通过系统调用,用户空间可以访问内核空间,那么如果一个用户空间想与另外一个用户空间进行通信怎么办呢?很自然想到的是让操作系统内核添加支持;传统的Linux通信机制,比如Socket,管道等都是内核支持的;但是Binder并不是Linux内核的一部分,它是怎么做到访问内核空间的呢?Linux的动态可加载内核模块(Loadable Kernel Module,LKM)机制解决了这个问题;模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行。这样,Android系统可以通过添加一个内核模块运行在内核空间,用户进程之间的通过这个模块作为桥梁,就可以完成通信了。在Android系统中,这个运行在内核空间的,负责各个用户进程通过Binder通信的内核模块叫做Binder驱动;驱动程序一般指的是设备驱动程序(Device Driver),是一种可以使计算机和设备通信的特殊程序。相当于硬件的接口,操作系统只有通过这个接口,才能控制硬件设备的工作;驱动就是操作硬件的接口,为了支持Binder通信过程,Binder使用了一种“硬件”,因此这个模块被称之为驱动。

Binder通信模型

现在我们知道,Binder驱动运行在内核空间,一般的程序运行在用户空间,都属于不同的进程,那么Binder通信是怎么设计的呢?Binder采用的是Client-Server方式,如果我们两个程序需要通信,那么其中一个就为Client,另外一个为Server。就像我们AIDL这个例子,一个服务端,一个客户端,但是仅仅这两个端是不够的,就像我们需要联系另外一个人,可以通过电话,如果没有电话,我们就联系不上这个人(不考虑其他的联系方式),就更别说和他交流了。所以我们需要一个电话簿,需要时,我们查查电话簿找到我们要联系的人,拨通电话,开始交流。那么在Binder通信模型必然也存在一个“电话簿”,它就是ServiceManager,整个系统只有一个电话簿,不是每一个进程都有自己的,它运行在内核空间内,我们的Client可以通过问它找到Server。注意!用户空间进程和内核空间的ServiceManager要跨进程通信才能看到“电话簿”啊!问题来了,找ServiceManager电话号码的“电话簿”在哪里呢?还有另外一个“电话簿”?找到这另外一个“电话簿”还需要另外再一个“电话簿”?……这个问题就成了“先有鸡还是先有蛋”。不过确实存在这个问题,那么Android是怎么去解决的呢?其实解决方法很简单,ServiceManager的“电话号码”是固定的,其他的所有进程都知道这个号码,需要时拨通这个号码就可以找到ServiceManager了。

Binder驱动

有了Client、Server和ServiceManager就可以通“电话”了吗?当然不是,还需要基站,而在这个模型中充当基站角色的就是我们Binder驱动,Server和ServiceManager通信、Client和Server通信都需要这个基站,Client、Server、ServiceManager和Binder驱动就是Binder通信模型四个重要的角色。

通信步骤

  1. ServiceManager建立(建立通信录);首先有一个进程向驱动提出申请为ServiceManager;驱动同意之后,ServiceManager进程负责管理Service(注意这里是Service而不是Server,因为如果通信过程反过来的话,那么原来的客户端Client也会成为服务端Server)不过这时候通信录还是空的,一个号码都没有。
  2. 各个Server向ServiceManager注册(完善通信录);每个Server端进程启动之后,向ServiceManager报告,我是zhangsan, 要找我请返回0x1234(这个地址没有实际意义,类比);其他Server进程依次如此;这样ServiceManager就建立了一张表,对应着各个Server的名字和地址;就好比B与A见面了,说存个我的号码吧,以后找我拨打10086;
  3. Client想要与Server通信,首先询问ServiceManager;请告诉我如何联系zhangsan,ServiceManager收到后给他一个号码0x1234;Client收到之后,开心滴用这个号码拨通了Server的电话,于是就开始通信了。

Binder机制跨进程原理

现在我们知道Binder通信的模型和以及他们各个角色是怎么配合的了,但是我们还不知道Binder驱动是怎么帮助我们完成通信的。这里只介绍原理,具体的分析留在后面。还是用这个AIDL例子来说明。上面的通信步骤我们知道,服务端将它的名字和地址告诉了ServiceManager,另外它还会告知ServiceManager它具备什么能力,客户端通过ServiceManager找到服务端说:我要服务端展示它的某一个能力,这里的能力就是某一个方法,例如例子中的getStr()方法。Binder驱动在这里面做了什么?它存储了所有服务端在那里注册的能力?其实不是,服务端向ServiceManager注册时,只是生成了一个引用,这个引用可以理解为指向服务端的一个引用,客户端可以通过ServiceManager得到这个引用,从而调用某个方法。不过要注意,这个引用并非服务端某个真实实现类的引用,ServiceManager和客户端持有的这个引用只是一个傀儡,相当于一个代理,这个傀儡可能和真实那个实现类一模一样,但是它不并不是在服务端那个真身。Client进程只不过是持有了Server端的代理;代理对象协助驱动完成了跨进程通信。

小结

Binder通讯的一些原理、过程和方式我们已经知道了,后面我们会介绍Client、Server、ServiceManager和Binder驱动,以及自己如何不用AIDL实现一个自己的进程间通讯。

参考:

进程地址空间与虚拟存储空间的理解

Binder学习指南

Android Bander设计与实现 - 设计篇

坚持原创分享,您的支持将鼓励我不断前行!